import matplotlib
import matplotlib.pyplot as plt
from matplotlib.pylab import hist, show
%matplotlib inline
import seaborn as sns; sns.set(style="ticks", color_codes=True)
import pandas as pd
import numpy as np
import re
Seguramente muchos de nosotros nos hemos encontrado una tarde de día sábado leyendo una noticia de actualidad en nuestro medio digital favorito. Finalizada la lectura, ante la inquietud de saber qué opiniones pudieron tener otros lectores de esta noticia, procedemos a ver la casilla de comentarios que generalmente está presente en estos medios, para encontrarnos con comentarios que siguen ciertos patrones comunes al tipo de noticia que se discuta: agresividad, nacionalismo, pesimismo, entre otros. Un ejemplo de esto es El Mercurio Online (Emol), medio digital relativamente masivo y famoso por la presencia de este tipo de comentarios, el cual es popularmente conocido por la naturaleza conservadora de quienes opinan, sobre todo en noticias que se refieren a cuestiones nacionales o sociales.
Con este contexto en mente, surge la pregunta de qué información de la población nacional podría reflejar el tipo de comentario que las personas están publicando en redes sociales. Claramente, para responder a esta interrogante haría falta agregar más variables a nuestro análisis, de manera tal de tener un panorama más completo de la realidad presente en nuestras redes sociales, agregando otros medios digitales de distintas tendencias ideológicas. Pero como una primera aproximación, comenzar a trabajar con Emol es suficiente.
Nuestra hipótesis inicial para trabajar nuestro dataset es la siguiente: “las personas que comentan en Emol se han vuelto más expresivas con el pasar del tiempo”.
Para poder responder a esta pregunta, tenemos que analizar los comentarios que las personas dejan en las noticias, ver qué características tienen estos comentarios y cómo se diferencian con otros (por ejemplo, ¿qué diferencias existen entre los comentarios de las secciones de Política con los de las secciones de Deportes?, ¿qué similitudes existen?).
Una pregunta preliminar es si existen grupos de comentarios que nazcan naturalmente dentro del dataset, lo cual se respondería aplicando un algoritmo de Clustering sobre ellos.
Si bien podemos generar preguntas preliminares para abordar en nuestros análisis, el tipo de preguntas se puede ampliar conociendo de mejor manera el dataset a trabajar. Para esto, se adjunta a continuación un análisis exploratorio de los datos, el cual nos da una idea más acabada de lo que el dataset contiene.
Hicimos webscrapping para obtener los comentarios de mil noticias del sitio web EMOL. Estas noticias fueron escogidas uniformemente distribuidas a lo largo del año para poder trabajar con un subconjunto del total de noticias sin perder la capacidad para explorar el comportamiento de las noticias y comentarios a lo largo del año.
Para cada noticia se dispone de su id, fecha, categoria, número de comentarios en el nivel mas alto y título.
Para cada una de estas noticias se descargaron todos sus respectivos comentarios. De los comentarios se dispone de su id, creador, id del creador, id de comentario padre (0 si es de primer nivel), metodo de autentificación, texto del comentario, likes, dislikes, denuncias, hora, nivel, sección de noticia.
news = pd.read_csv("2017_emol_news_small.tsv", sep="\t")
comment = pd.read_csv("2017_emol_comments_small.tsv", sep="\t")
print(news.columns) #variables
#print(news.describe(include="all"))
#print(news.dtypes) #estructura
#noticias
print("\ncantidad de noticias: ",len(pd.unique(news['idNoticia'])))
#fecha
print("\nfecha de las noticias")
print("Desde el:",news['fecha'].min(), ", hasta el: ",news['fecha'].max())
#categorias
print("\nFrecuencia por categorias:\n",news['categoria'].value_counts())
#nTopLevelComments
print("\ncantidad de noticias con comentarios:",len(news[news['nTopLevelComments']!=0]))
print("Descipción comentarios por noticias: \n",news["nTopLevelComments"].describe())
#titulo -> se deja para un prox procesamiento de texto
En el siguiente gráfico de torta se muestra la proporción de noticias de cada categoría en el 2017, donde se puede observar que las 3 categorías que más noticias tienen son Nacional, Internacional y Deportes, seguidas de cerca de espectáculos y Economia. Frente a esto se muestra una visualización con las 4 categorías con más noticias y su evolución durante el año.
print(np.array(news['categoria'].value_counts().index))
df = pd.DataFrame({'categoria': np.array(news['categoria'].value_counts())},
index = np.array(news['categoria'].value_counts().index))
df['categoria']=df['categoria']/sum(df['categoria'])
#print(df['categoria'])
#print(df.index)
colors=['']
print("\n\t\t------Porcentaje de noticias por categoría------")
labels=df.index
plt.figure(figsize=(7,7))
plt.pie(df['categoria'], labels=df.index,autopct='%1.1f%%', startangle=90)
plt.legend(labels, loc="best")
plt.axis('equal')
plt.tight_layout()
plt.show()
df=news
mes= []
for i in range(0,len(df)):
m=int(df['fecha'][i][5:7])
mes.append(m)
df['mes']=mes
df['noticias']=np.repeat(1,len(news))
df=df[(df['categoria']=="Nacional") | (df['categoria']=="Deportes") | (df['categoria']=="Espectaculos")| (df['categoria']=="Economia")]
df=df.groupby(['mes','categoria'])['noticias'].agg(sum)
df=pd.DataFrame({'mes':df.index.get_level_values('mes'),'categoria':df.index.get_level_values('categoria'), 'noticias':np.array(df)})
#print(df)
print("------------------ Cantidad de noticias para las categorias importantes en el tiempo----------------------------")
#flatui = ["#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"]
#sns.palplot(sns.color_palette(flatui))
g = sns.FacetGrid(df, col="categoria",hue="categoria", palette=["r","b","g","c"])
g = g.map(plt.bar,"mes", "noticias",edgecolor="w")
A continuación se presenta la distribución de comentarios que tuvieron las noticias en el 2017, donde se hace una distinción entre los comentarios totales y los comentarios de primer nivel solamente.
##Merge entre las tablas
df=comment
df['comentarios']=np.repeat(1,len(comment))
df=df.groupby(['pageCmsId'])['comentarios'].agg(sum)
df=pd.DataFrame({'idNoticia':df.index, 'comentarios':np.array(df)})
df=pd.merge(news,df, how= 'outer', on='idNoticia')
df=df[['fecha','categoria','comentarios']]
df['comentarios']=df['comentarios'].fillna(0)
plt.figure(2)
print("------Frecuencia de la cantidad de comentarios------")
plt.subplot(121)
plt.hist(df['comentarios'], 300)
plt.title("Frecuencia de comentarios")
plt.ylim(0,180)
plt.xlim(0,150)
plt.xlabel("Cantidad de comentarios")
plt.ylabel("Cantidad de noticias")
plt.grid(True)
#print("cantidad de noticias con más de 50 comentarios: ",len(df[df['comentarios']>50]))
#print("cantidad de noticias con 0 comentarios: ",len(df[df['comentarios']==0]))
#print("cantidad de noticias con más de 0 comentarios: ",len(df[df['comentarios']!=0]))
plt.subplot(122)
plt.hist(news['nTopLevelComments'], 200)
plt.title("Frecuencia de comentarios de primer nivel")
plt.ylim(0,180)
plt.xlim(0,150)
plt.xlabel("Cantidad de comentarios")
plt.ylabel("Cantidad de noticias")
plt.grid(True)
plt.subplots_adjust(top=1.5, bottom=0.08, left=0.10, right=4, hspace=0.34,
wspace=0.1)
#print("cantidad de noticias con más de 50 comentarios: ",len(news[news['nTopLevelComments']>50]))
#print("cantidad de noticias con 0 comentarios: ",len(news[news['nTopLevelComments']==0]))
#print("cantidad de noticias con más de 0 comentarios: ",len(news[news['nTopLevelComments']!=0]))
print("cantidad de noticias con más de 50 comentarios: ",len(df[df['comentarios']>50]))
print("cantidad de noticias con 0 comentarios: ",len(df[df['comentarios']==0]))
print("cantidad de noticias con más de 0 comentarios: ",len(df[df['comentarios']!=0]))
print("\ncomentarios por categoria:\n", comment['pageSection'].value_counts(normalize=True))
#print("\nnoticias por categoria:\n",news['categoria'].value_counts())
Ahora analizamos la cantidad de comentarios promedio por noticia en cada categoria, donde se observa nuevamente que la categoria Nacional es la que más comentarios tiene por noticias, donde luego vienen las categorias de deportes y economia, seguidas por Internacional y Espectaculos. En este nuevo análisis la categoria Nacional siguie en la cabeza, pero economía a paso a ser la 3 más relevante a pesar de no tener la mayor cantidad de noticias, esto quiere decir que a pesar de no tener tantas noticias como Espectaculos e Internacional es una de las más comentadas.
df['noticias']=np.repeat(1,len(df))
df1=df.groupby(['categoria'])['comentarios'].mean()
print("\nComentarios por noticia para las diferentes categorias: \n",df1)
df1=pd.DataFrame({'categoria':df1.index, 'comentarios/noticias':np.array(df1)})
print("\n------Comentarios/noticias para las diferentes categorias------")
sns.barplot(x="comentarios/noticias",y='categoria',data=df1, hue_order="com_not")
En este tercer gráfico se muestra la evolución temporal de la cantidad de comentarios por mes a lo largo del 2017 para las categorías mas importantes, donde se observa un orden marcado de volumen de comentarios/noticias comenzando por Nacional seguido por Deportes, Economia y Espectaculos.
mes= []
for i in range(0,len(df)):
m=int(df['fecha'][i][5:7])
mes.append(m)
df['mes']=mes
df1=df[(df['categoria']=="Nacional") | (df['categoria']=="Deportes") | (df['categoria']=="Espectaculos")| (df['categoria']=="Economia")]
df1=df1.groupby(['categoria','mes'])['comentarios'].mean()
df1=pd.DataFrame({'categoria':df1.index.get_level_values('categoria'),'mes':df1.index.get_level_values('mes'), 'comentarios':np.array(df1)})
print("------------------ Cantidad de comentarios/noticias en el año 2017 por categoria----------------------------")
#flatui = ["#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"]
#sns.palplot(sns.color_palette(flatui))
g = sns.FacetGrid(df1, col="categoria",hue="categoria", palette=["r","b","g","c"])
g = g.map(plt.bar,"mes", "comentarios",edgecolor="w")
Primero hacemos un analisis de los deciles para la cantidad de comentarios de cada usuaria. La tabla que se muestra nos dice que la mitad de los usuarios comenta a lo mas 1 vez y que el 90% de los usuarios comenta a lo más 6 veces.
Esto levanta el interes de ver que porcentaje de usuarios comenta una sola vez. Vemos que el 55% de los usuarios comenta una sola vez.
print("Cantidad de usuarios que comentaron: ",len(comment['creatorId'].unique()))
print("Cantidad comentarios: ",len(comment['creatorId']))
#print(pd.qcut(comment['creatorId'],10, labels=False))
df1=comment.groupby(['creatorId'])['comentarios'].agg(sum)
df1=pd.DataFrame({'creatorId':df1.index, 'comentarios':np.array(df1)})
print("\nLos deciles de comentarios por usuarios son los siguientes:")
print(np.array(df1['comentarios'].quantile([.1, .2, .3, .4, .5, .6 ,.7,.8,.9, 1])))
print("\nCantidad de usuarios que comentaron solo una vez en el 2017: ")
print(len(df1[df1['comentarios']==1])/len(df1))
df1=df1.sort_values(['comentarios'], ascending=False)
print("\n En la siguiente tabla se ve los usuarios que comentaron más veces en el 2017")
print(df1[0:10])
Por otra parte es escogió analizar la participación de los usuarios más activos dentro de la plataforma. Para esto, el siguiente gráfico muestra los comentarios a lo largo del año de los 6 usuarios más activos separados por categoría. Se puede ver en el gráfico que que 5 de los 6 usuarios comentan mayoritariamente en la categoría nacional y solo uno lo hace de gran manera en deportes, siendo además la única categoria donde participa dicho usuario.
df=comment
mes= []
for i in range(0,len(df)):
m=int(df['time'][i][5:7])
mes.append(m)
df['mes']=mes
df1=df.groupby(['creatorId','pageSection','mes'])['comentarios'].agg(sum)
df1=pd.DataFrame({'creatorId':df1.index.get_level_values('creatorId'),'categoria':df1.index.get_level_values('pageSection'),
'mes':df1.index.get_level_values('mes'), 'comentarios':np.array(df1)})
print("------------------ Cantidad de comentarios/noticias en el año 2017 para usuarios top----------------------------")
df1=df1[(df1['categoria']=="Nacional") | (df1['categoria']=="Deportes") | (df1['categoria']=="Espectaculos")| (df1['categoria']=="Economia")]
df2=df1[(df1['creatorId']==156470) | (df1['creatorId']== 241785) | (df1['creatorId']==7729)]
g = sns.FacetGrid(df2, col="creatorId",hue="categoria", palette=["r","b","g","c"], legend_out=True)
g = g.map(plt.plot,"mes", "comentarios").add_legend()
df2=df1[(df1['creatorId']==161887) | (df1['creatorId']== 925) | (df1['creatorId']==451291)]
g = sns.FacetGrid(df2, col="creatorId",hue="categoria", palette=["r","b","g","c"])
g = g.map(plt.plot,"mes", "comentarios").add_legend()
print(comment.columns) #variables
col_comment=['likes','dislikes','denounces','level','pageSection']
df=comment[col_comment]
#likes | dislikes | denounces | level
df=df[(df['pageSection']=='Nacional') | (df['pageSection']=='Economia') | (df['pageSection']=='Espectaculos') | (df['pageSection']=='Deportes')]
print("------------------Gráfico de correlaciones para reacciones de comentarios----------------------------")
sns.pairplot(df, hue="pageSection", kind='reg')
col_comment=['likes','dislikes','denounces','time','level']
df=comment[col_comment]
corr = df.corr()
mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
cmap = sns.diverging_palette(220, 10, as_cmap=True)
print("--------Matriz de correlación-------------")
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0,
square=True, linewidths=.5)
Para esta sección se analiza el texto desde el punto de vista de la cantidad de caracteres y el tipo de caracteres. En el siguiente gráfico se muestra la distribución de la cantidad de caracteres de los comentarios.
comentarios=np.array(comment['text'])
cant=[]
for i in range(0,len(comentarios)):
cant.append(len(comentarios[i]))
print("--------Distribución cantidad de caracteres en los comentarios-------------")
plt.hist(cant, bins=100)
plt.title("Distribución cantidad de caracteres en los comentarios")
Ahora se grafica el promedio de caracteres de los comentarios por categoría, agregando el promedio global como punto de comparación.
df=comment[['text','pageSection']]
df['cant']=cant
#df.loc[:,('cant')]=cant
df1=df.groupby(['pageSection'])['cant'].mean()
df1=pd.DataFrame({'pageSection':df1.index, 'cant':np.array(df1)})
print("--------Cantidad promedio de caracteres en comentarios-------------")
sns.barplot(y='pageSection', x='cant', data=df1)
plt.title("Cantidad promedio de caracteres en comentarios")
En los siguientes dos graficos se muestra la cantidad promedio de caracteres exclamativos (!) e interrogativos (?) para cada categoria.
def contar(com,c):
a=0
for i in range(0,len(com)):
if(com[i]==c):
a=a+1
return a
excl=[]
preg=[]
for i in range(0,len(comentarios)):
excl.append(contar(comentarios[i],"!"))
preg.append(contar(comentarios[i],"?"))
df1=pd.DataFrame({'pageSection':comment['pageSection'], 'excl':excl, 'preg':preg})
df1=df1.groupby(['pageSection'])['excl','preg'].mean()
df1=pd.DataFrame({'pageSection':df1.index, 'excl': np.array(df1['excl']), 'preg': np.array(df1['preg'])})
##Cantidad de símbolos "!" por comentarios
print("--------Cantidad de símbolos '!' por comentarios -------------")
sns.barplot(x='excl', y='pageSection', data=df1)
plt.title("Cantidad de símbolos '!' por comentarios")
##Cantidad de símbolos "?" por comentarios
print("--------Cantidad de símbolos '?' por comentarios -------------")
sns.barplot(x='preg', y='pageSection', data=df1)
plt.title("Cantidad de símbolos '?' por comentarios")
topwords = pd.read_csv("topwordsglobal.csv", sep=",")
print("--------Top words en los comentarios -------------")
sns.barplot(x='002',y='001',data=topwords)
plt.xlabel("cantidad de apariciones")
plt.ylabel("palabras top")
plt.title("Top de palabras con más apariciones")
topwords_sec = pd.read_csv("topwordssection.csv", sep=",")
col=['Deportespalabra', 'Deportescantidad', 'Economiapalabra',
'Economiacantidad', 'Espectaculospalabra', 'Espectaculoscantidad',
'Nacionalpalabra','Nacionalcantidad']
print(topwords_sec[col])
En esta parte vamos a trabajar con el texto de los comentarios donde vamos a crear el bags of words, luego eliminaremos las stop words (palabras que no nos entregan información como conjunciones y preposición como otras), luego vamos a hacer un proceso de stemming, donde llevaremos las palabras a su raíz y buscaremos un diccionario de sinónimos para no tener rebundancia en las palabras. Finalmente, con esto crearemos el vector space model para realizar diferentes procedimientos sobre los comentarios.
from sklearn.feature_extraction.text import CountVectorizer
from nltk import word_tokenize
import nltk.stem
from sklearn import metrics
from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer('spanish')
def extract_words(sentence):
ignore_words = np.array(pd.read_csv("stop_words.txt"))
words = re.sub("[^\w]", " ", sentence).split() #nltk.word_tokenize(sentence)
words_cleaned = [w.lower() for w in words if w not in ignore_words]
return words_cleaned
def stemming(sentence):
stems = [stemmer.stem(w) for w in sentence]
return stems
def tokenize_sentences(sentences):
words = []
for sentence in sentences:
w = extract_words(sentence)
w = stemming(w)
words.extend(w)
words = sorted(list(set(words)))
return words
def fusion(vec,s):
string=""
vec=list(vec)
for i in vec:
string=string+s+str(i)
return string
def bagofwords(sentence, words):
sentence_words = extract_words(sentence)
# frequency word count
bag = np.zeros(len(words))
for sw in sentence_words:
for i,word in enumerate(words):
if word == sw:
bag[i] += 1
return np.array(bag)
def cosine_similarity(v1,v2):
"compute cosine similarity of v1 to v2: (v1 dot v2)/{||v1||*||v2||)"
sumxx, sumxy, sumyy = 0, 0, 0
for i in range(len(v1)):
x = v1[i]; y = v2[i]
sumxx += x*x
sumyy += y*y
sumxy += x*y
return sumxy/math.sqrt(sumxx*sumyy)
"""
filtro=comment[(comment['pageSection']=="Nacional")|(comment['pageSection']=="Economia")|(comment['pageSection']=="Deportes")|(comment['pageSection']=="Espectaculos")]
ids=list(filtro['id'])
comentarios=list(filtro['text'])
bags_words = tokenize_sentences(comentarios)
#for com in comentarios:
# bagofwords(com, bags_words)
vectorizer = CountVectorizer(analyzer = "word", tokenizer = None, preprocessor = None, stop_words = None, max_features = 5000)
train_data_features = vectorizer.fit_transform(bags_words)
f = open("vector_space_model.txt",'w')
vec=[]
for i in range(0,len(comentarios)):
vec.append(vectorizer.transform([comentarios[i]]).toarray())
vectores=fusion(vec[i][0],";")
f.write(str(ids[i])+ vectores+"\n")
f.close()
"""
"""
bla=pd.read_csv("vector_space_model.txt", sep=";")
data=bla[bla.columns[1:]]
from sklearn.cluster import KMeans
from sklearn.metrics import davies_bouldin_score
kmeans = KMeans(n_clusters=3, random_state=1).fit(data)
labels = kmeans.labels_
davies_bouldin_score(data, labels)
metrics.calinski_harabaz_score(data, labels)
"""
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans, AgglomerativeClustering
from sklearn.metrics import adjusted_rand_score
from nltk import word_tokenize
import nltk.stem
from sklearn import metrics
from nltk.stem import SnowballStemmer
stemmer = SnowballStemmer('spanish')
#filtro=list(comment['text'][(comment['pageSection']=="Nacional")|(comment['pageSection']=="Economia")|(comment['pageSection']=="Deportes")|(comment['pageSection']=="Espectaculos")])
filtro=list(comment['text'][(comment['pageSection']=="Espectaculos")|(comment['pageSection']=="Internacional")])
print(len(filtro))
stops=list(pd.read_csvs("stop_words.txt")['stop'])
#stops=stemming(list(pd.read_csv("stop_words.txt")['stop']))
vectorizer = TfidfVectorizer(analyzer='word', stop_words=stops, lowercase=True)#, preprocessor=stemmer.stem)
X = vectorizer.fit_transform(filtro)
true_k = 2
model = KMeans(n_clusters=true_k, init='k-means++', max_iter=100, n_init=1)
#model = AgglomerativeClustering(n_clusters=true_k,affinity='cosine', linkage='average')
"""
print(len(comment[comment['pageSection']=="Nacional"]))
print(type(X))
print(X.shape)
print(type(X[17013]))
"""
model.fit(X)
print("Top terms per cluster:")
order_centroids = model.cluster_centers_.argsort()[:, ::-1]
terms = vectorizer.get_feature_names()
for i in range(true_k):
print("Cluster %d:" % i),
for ind in order_centroids[i, :10]:
print(' %s' % terms[ind]),
print
print("\n")
print("Prediction")
from sklearn import metrics
#from sklearn.metrics import pairwise_distances
labels = model.labels_
print(metrics.silhouette_score(X, labels))
#metrics.calinski_harabaz_score(X, labels)
#print(model.inertia_)
print(model.cluster_centers_[4])
print(metrics.davies_bouldin_score(X.toarray(), labels))
# ## Vectores para respuestas.
# Este punto consiste en cargar determinados valores para cada palabra que tienen una caracterización semantica. esto permite poder buscar un signficado semántico a casa respuesta.
from gensim.models import Word2Vec
from gensim.models.keyedvectors import KeyedVectors
wordvectors_file_vec = 'fasttext-sbwc.3.6.e20.vec.gz'
cantidad = 100000
wordvectors = KeyedVectors.load_word2vec_format(wordvectors_file_vec, limit=cantidad).wv
# ## Promedio Vectorial
# La siguiente función va a permitir caracterizar los comentarios posteriormente.
import pandas as pd
import numpy as np
from functools import reduce
vector_zero = np.zeros(shape=wordvectors["hola"].shape)
## fución sumas y evitar errores ortográficos
def word_vec_sum(partial, word):
# este try es para evitar problemas de errores ortográficos
try:
return partial + wordvectors[word.lower()]
except:
return partial
##SUMA DE LOS VECTORES, NUEVO VECTORES DE DIMENSION 300
def sentence_word_vec(sentence):
return reduce(word_vec_sum, sentence.split(" "), vector_zero) / len(spanish_word_list)
path = 'comentarios.txt'
##seleccionamos palabras en español, cargamos todo el diccionario
with open(path, 'r', encoding="utf-8") as f:
spanish_word_list = f.read().splitlines()
len(spanish_word_list)
##vectorizamos todas las pablar del español
spanish_words_vectors = pd.DataFrame({'WORD': spanish_word_list, "VECTOR" : list(map(sentence_word_vec, spanish_word_list))})
spanish_words_vectors.loc[:, "VECTOR"] = spanish_words_vectors["VECTOR"].apply(lambda x: None if (x == vector_zero).all() else x)
spanish_words_vectors = spanish_words_vectors.dropna(subset=['VECTOR'])
# ## Pruebas asociadas
# Los pasos anteriores, permiten poder realizar la caretización de una frase como un vector (es decir, un "número" que se caracteriza según un espacio semantico.
sentencia='economia crecimiento'
sentence_word_vec(sentencia)
data=data.dropna(subset=["respuestas"])
data.info(verbose=True)
z=data['u_local'].value_counts().plot(kind='bar')
Para esta iteración, se cuenta con un dataset de comentarios de Emol y otros periódicos electrónicos etiquetados por expertas. La idea será generar clasificadores que puedan discernir entre distintos comentarios y ver si se generan clases clasificables dentro de los comentarios, para luego extender esta tarea a los comentarios de Emol extraídos por nuestra cuenta.
import numpy as np
import pandas as pd
import re
from nltk.stem import SnowballStemmer
from unidecode import unidecode
import spacy
import sys
import random
data_raw = pd.read_csv('datos_profe/FINAL DATABASE - Magdalena Saldaña.csv', sep='\t')
print("Leemos las columnas de los datos: \n")
print(data_raw.columns)
data=data_raw.drop(columns=['CASODEF', 'Caso (old)', 'Noticia', 'Fecha', 'Diario',
'Género usuario', 'Usuario anónimo', 'Codificación',
'codificado en '])
# se realiza copia de Comentario, no se deberia modificar data de ahora en adelante
# data se usa en el futuro para la clasificacion
comments = pd.DataFrame(data['Comentario'].copy())
print("Miramos el dataset data y los comentarios que se extrajeron:\n")
print(data.head())
print(comments.head())
Ahora, se trabajará sobre los comentarios de la siguiente manera:
#Se agregan las columnas de interrogación y pregunta
comments = comments.assign(simbolo_interrogacion=pd.Series([(1 if ('?' in w) else 0) for w in comments.Comentario]))
comments = comments.assign(simbolo_exclamacion=pd.Series([(1 if ('!' in w) else 0) for w in comments.Comentario]))
print(comments[['Comentario', 'simbolo_interrogacion']].head(n=10))
print(comments[['Comentario', 'simbolo_exclamacion']].head(n=10))
# saca del string w todas las palabras que se encuentren en el set s
# ademas saca todo lo que no sea alfanumerico
# deja acentos, deja mayusculas
def remove_words(w, s):
#w = re.sub(r'[^a-zA-Z0-9\s]', ' ', w)
w = re.sub(r'[\W\s]', ' ', w)
return ' '.join([w for w in w.split() if w not in s])
# se sacan los nombre de usuario (siempre y cuando aparezcan textuales)
names = [w.rstrip() for w in open('datos_profe/names_list.txt', 'r').readlines()]
#print(len(names))
#print(names[0:10])
comments['Comentario'] = [remove_words(w, names) for w in comments['Comentario']]
#print(comments.head(n=10))
# aca va la lematizacion (parecido al stemming), se hace antes de sacar stop words,
# acentos y mayusculas para no perder significado y que el lemmatizer no se pierda
nlp = spacy.load('es')
comments['Comentario'] = [' '.join([token.lemma_ for token in nlp(w)])
for w in comments['Comentario']]
print("Muestra de comentarios luego de la lematización: \n")
print(comments['Comentario'].head(n=10))
# sacar acentos y mayusculas
comments['Comentario'] = [unidecode(w.lower()) for w in comments['Comentario']]
# sacar stop words
stop_words = set([unidecode(w.rstrip().lower()) for w in open('stopwords-es.txt', 'r').readlines()])
#print(stop_words)
comments['Comentario'] = [' '.join(list(filter(lambda x : x not in stop_words, w.split()))) for w in comments['Comentario']]
# sacar numeros
comments['Comentario'] = [re.sub('\d', '', w) for w in comments['Comentario']]
# se guardan palabras de largo tres o más
comments['Comentario'] = [' '.join(list(filter(lambda x : len(x) > 2, w.split()))) for w in comments['Comentario']]
print("Muestra de los comentarios luego de la limpieza: \n")
print(comments['Comentario'].head(n=10))
Con todo lo anterior, generamos un set de palabras para tener el espacio completo y sin repeticiones de las palabras que aparecen en todos los comentarios del dataset etiquetado.
# ahora se hará un set con todas las palabras distintas para poder
# generar luego las columnas de la vectorizacion y luego poner los 1's y 0's correspondientes
# para cada comentario
# Se guardan todas las palabras para que sea facil cargarlas despues
s = set()
for c in comments['Comentario']:
for w in c.split():
s.add(w)
s = sorted(s)
with open('bag_of_words.txt', 'w') as out:
for w in s:
out.write(w + '\n')
print(f"Cantidad de comentarios: {len(comments)}")
print('Palabras :', len(s))
print('bytes:', sys.getsizeof(s))
# Aca usamos un bag of words y se crea la vectorizacion para cada comentario
# MAS las columnas de tiene? y tiene!
# Se hace un diccionario con la posicion en la matriz donde va cada palabra de la bag of words
d = dict()
n = len(comments)
for i, w in enumerate(s):
d[w] = i
arr = np.zeros([n, len(s) + 2])
for i, c in enumerate(comments.Comentario):
for w in c.split():
arr[i][d[w]] += 1
# ahora le agrego las dos ultimas columnas
arr[:, -1] = comments['simbolo_exclamacion'].tolist()
arr[:, -2] = comments['simbolo_interrogacion'].tolist()
print(f"Dimensión de la matriz o espacio de las palabras: {arr.shape}")
print('peso en memoria de arr:', sys.getsizeof(arr)/1e6, 'mb')
Teniendo ya los comentarios vectorizados, estamos en condiciones de alimentar a un clasificador para ver si es posible automatizar la detección de ciertos tipos de expresiones.
#Vamos a comenzar a clasificar los datos. Usaremos las clases más etiquetadas que son
#"Groseria/Lenguaje vulgar" e "Insulto/Sobrenombre".
classdata = pd.DataFrame(arr)
y1 = data['Grosería/Lenguaje vulgar']
print("Distribución de clase: 0 es otro comentario, 1 es groseria")
print(y1.value_counts())
y2 = data['Insulto/Sobrenombre']
print("Distribución de clase: 0 es otro comentario, 1 es insulto/sobrenombre")
print(y2.value_counts())
#Vemos que las clases están fuertemente desbalanceadas, por lo que es necesario subsamplear la muestra
#Generamos dos tablas nuevas para cada caso
raw1 = classdata.join(y1) #Grosería
raw2 = classdata.join(y2) #Insulto
idx1 = np.random.choice(raw1.loc[raw1['Grosería/Lenguaje vulgar'] == 0].index, size=4800, replace=False)
idx2 = np.random.choice(raw2.loc[raw2['Insulto/Sobrenombre'] == 0].index, size=3245, replace=False)
data1 = raw1.drop(raw1.iloc[idx1].index) #Clase Grosería equilibrada
data2 = raw2.drop(raw2.iloc[idx2].index) #Clase Insulto equilibrada
Dado que las clases están desbalanceadas, será necesario hacer un subsampling de la muestra para así entrenar de mejor manera a los clasificadores.
#Revisamos que las nuevos datos estén equilibrados
print("Distribución de clase: 0 es otro comentario, 1 es groseria")
aa,be = data1['Grosería/Lenguaje vulgar'].value_counts()
print(data1['Grosería/Lenguaje vulgar'].value_counts())
print("Proporción: "+str(round(aa*1.0/be,4)))
print("Distribución de clase: 0 es otro comentario, 1 es insulto/sobrenombre")
ce,de = data2['Insulto/Sobrenombre'].value_counts()
print(data2['Insulto/Sobrenombre'].value_counts())
print("Proporción: "+str(round(ce*1.0/de,4)))
#Y preparamos los datos a usarse
X1 = data1[data1.columns[:-1]]
y1 = data1[data1.columns[-1]]
X2 = data2[data2.columns[:-1]]
y2 = data2[data2.columns[-1]]
Con los datos balanceados, estamos en condiciones de entrenar nuestros clasificadores. Para evaluar su desempeño, usaremos una función vista en un laboratorio, utilizando 30 tests por clasificador. Las métricas entregadas por esta función serán f1, precisión y recall.
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score
from sklearn.metrics import confusion_matrix
#Con esto ya podemos comenzar a entrenar nuestros modelos, definimos entonces una función
#para correr y evaluar los clasificadores
def runclassifier(clf, X, y, num_tests=30):
metrics = {'f1-score': [], 'precision': [], 'recall': []}
confusion = 0
for _ in range(num_tests):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.30, stratify=y)
clf.fit(X_train,y_train)
predictions=clf.predict(X_test)
metrics['f1-score'].append(f1_score(y_test, predictions)) # X_test y y_test deben ser definidos previamente
metrics['recall'].append(recall_score(y_test, predictions))
metrics['precision'].append(precision_score(y_test, predictions))
confusion= confusion + np.array(confusion_matrix(y_test, predictions))
return metrics, confusion/num_tests
A continuación se muestran los resultados del entrenamiento de los clasificadores. Los clasificadores utilizados son:
from sklearn.svm import LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.dummy import DummyClassifier
#Generamos 4 clasificadores y un Dummy y los evaluamos sobre el dataset 1, "Groserías/Lenguaje vulgar
nb1 = ("Naive Bayes", GaussianNB())
rf1 = ("Random Forest", RandomForestClassifier(n_estimators=100))
svm1 = ("SVM Lineal", LinearSVC())
dt1 = ("Decision Tree", DecisionTreeClassifier())
dummy1 = ("Dummy", DummyClassifier(strategy='stratified'))
cfs1 = [nb1, rf1, svm1, dt1, dummy1]
print("Clasificadores para el dataset Groserías/Lenguaje vulgar: \n")
for name, clf in cfs1:
metrics, confusion = runclassifier(clf, X1, y1)
print("----------------")
print("Resultados para clasificador:",name)
print("Precision promedio:",np.array(metrics['precision']).mean())
print("Recall promedio:",np.array(metrics['recall']).mean())
print("F1-score promedio:",np.array(metrics['f1-score']).mean())
print("matrix confusion promedio:\n", (confusion))
print("----------------\n\n")
#Repetimos, pero ahora sobre el dataset de "Insulto/Sobrenombre"
nb2 = ("Naive Bayes", GaussianNB())
rf2 = ("Random Forest", RandomForestClassifier(n_estimators=100))
svm2 = ("SVM Lineal", LinearSVC())
dt2 = ("Decision Tree", DecisionTreeClassifier())
dummy2 = ("Dummy", DummyClassifier(strategy='stratified'))
cfs2 = [nb2, rf2, svm2, dt2, dummy2]
print("Clasificadores para el dataset Insulto/Sobrenombre: \n")
for name, clf in cfs2:
metrics, confusion = runclassifier(clf, X2, y2)
print("----------------")
print("Resultados para clasificador: ",name)
print("Precision promedio:",np.array(metrics['precision']).mean())
print("Recall promedio:",np.array(metrics['recall']).mean())
print("F1-score promedio:",np.array(metrics['f1-score']).mean())
print("matrix confusion promedio:\n", (confusion))
print("----------------\n\n")
Con lo anterior se pudo ver que las etiquetas seleccionadas para clasificación son bien recibidas por algunos métodos, siendo Random Forest el que mejor resultados arroja. Ahora lo que haremos será seleccionar comentarios de Emol del dataset anterior al azar para intentar clasificarlos.
#Cargamos datos de emol y nos quedamos con los primeros 1000 comentarios
emol = pd.read_csv("2017_emol_comments_small.tsv", sep="\t")
emoltext = pd.DataFrame(emol['text'][0:1000])
emoltext = emoltext.assign(simbolo_interrogacion=pd.Series([(1 if ('?' in w) else 0) for w in emoltext.text]))
emoltext = emoltext.assign(simbolo_exclamacion=pd.Series([(1 if ('!' in w) else 0) for w in emoltext.text]))
emoltext['text'] = [remove_words(w, names) for w in emoltext['text']]
#Lematización, toma tiempo
emoltext['text'] = [' '.join([token.lemma_ for token in nlp(w)])
for w in emoltext['text']]
# sacar acentos y mayusculas
emoltext['text'] = [unidecode(w.lower()) for w in emoltext['text']]
# sacar stop words
emoltext['text'] = [' '.join(list(filter(lambda x : x not in stop_words, w.split()))) for w in emoltext['text']]
# sacar numeros
emoltext['text'] = [re.sub('\d', '', w) for w in emoltext['text']]
# se guardan palabras de largo tres o más
emoltext['text'] = [' '.join(list(filter(lambda x : len(x) > 2, w.split()))) for w in emoltext['text']]
print(emoltext['text'].head(n=10))
ceros = np.zeros([len(emoltext), len(s) + 2])
#generamos la vectorizacion para los comentarios nuevos
for i, c in enumerate(emoltext.text):
for w in c.split():
if(w in s):
ceros[i][d[w]] += 1
# agregamos las columnas de tiene? y tiene!
ceros[:, -1] = emoltext['simbolo_exclamacion'].tolist()
ceros[:, -2] = emoltext['simbolo_interrogacion'].tolist()
#Ya teniendo los comentarios vectorizados, podemos generar un dataframe y alimentar un clasificador
#del tipo Random Forest entrenado con los datos etiquetados.
emolvec = pd.DataFrame(ceros)
emolforest1 = RandomForestClassifier(n_estimators=100) #Clasifica Groserías/Lenguaje vulgar
emolforest2 = RandomForestClassifier(n_estimators=100) #Clasifica Insulto/Sobrenombre
emolforest1.fit(X1,y1)
emolforest2.fit(X2,y2)
print("Terminó entrenamiento de los Random Forest")
#Generamos las predicciones
groserias = emolforest1.predict(emolvec)
insultos = emolforest2.predict(emolvec)
grosindices = [i for i, x in enumerate(groserias) if x == 1]
print(f"Cantidad de Groserias segun el clasificador: {len(grosindices)}")
insuindices = [i for i, x in enumerate(insultos) if x == 1]
print(f"Cantidad de Insultos segun el clasificador: {len(insuindices)}")
colisiones = set(grosindices).intersection(set(insuindices))
print(f"Cantidad de colisiones: {len(colisiones)}")
onlygros = [x for x in grosindices if x not in colisiones]
onlyinsu = [x for x in insuindices if x not in colisiones]
random.seed(567)
grsample = random.sample(onlygros, 10) #Elegimos 30 indices al azar para groserías y para insultos
insample = random.sample(onlyinsu, 10)
Con todo lo que tenemos, elegimos al azar 10 comentarios etiquetados por nuestro clasificador, para así ver la calidad de la clasificación.
print("Según el clasificador, contienen groserías o lenguaje vulgar los siguientes comentarios:\n ")
for i,ind in enumerate(grsample):
print(str(i)+" "+emol['text'][ind])
print()
print("Según el clasificador, contienen insultos o sobrenombres los siguientes comentarios:\n ")
for i,ind in enumerate(insample):
print(str(i)+" "+emol['text'][ind])
print()
Con esto vemos que los comentarios de Emol pueden ser clasificados a partir del contenido que posean, pero en este caso la clasificación no es tan certera como podría llegar a haber sido. Esto se debe a que sólo estamos evaluando las palabras que contiene cada comentario, sin hacer una distinción entre palabras de distinto tipo, ni asignándole peso a cierto tipo de palabras. Por ejemplo, en una oración es más probable que nos diga más información un sustantivo que un artículo, o un verbo más que un pronombre.